package transporter

import (
	"barge/pkg/docker"
	"barge/pkg/manager"
	"encoding/json"
	"fmt"
	"github.com/pkg/errors"
	"github.com/rs/zerolog/log"
	"gopkg.in/yaml.v3"
	"io"
	"os"
	"path"
	"strings"
)

const (
	StepLogin = iota
	StepPull
	StepTag
	StepPush
)

type Option struct {
	Quiet    bool
	Stdout   io.Writer
	Stderr   io.Writer
	Center   *manager.Center
	Registry *manager.Registry
}

// Transporter 镜像传输器, 应只关注单个镜像的多架构传输, 支持镜像特性
type Transporter struct {
	Executor
	*Option
	step        int
	SourceImage *manager.ImageUnit
	TargetImage *manager.ImageUnit
	workspace   string
	savePath    string
}

func NewTransporter(opt *Option, e Executor) *Transporter {
	return &Transporter{
		Option:    opt,
		Executor:  e,
		workspace: "/tmp/barge",
	}
}

func (t *Transporter) refreshTargetImage() {
	t.TargetImage = manager.NewImageUnit(t.Registry.Address+"/"+t.SourceImage.Reference.Path(), t.SourceImage.Arch, t.SourceImage.IsPartial)
}

// Push 推送镜像, 传入的镜像必须是本地单架构镜像, 不支持bundle解析
func (t *Transporter) Push(image *manager.ImageUnit) error {
	t.SourceImage = image
	t.refreshTargetImage()
	if err := t.login(); err != nil {
		err = errors.Wrap(err, "登录仓库失败")
		return err
	}
	if err := t.tag(); err != nil {
		err = errors.Wrap(err, "构建标签失败")
		return err
	}
	if err := t.push(); err != nil {
		err = errors.Wrap(err, "推送镜像失败")
		return err
	}
	return nil
}

func (t *Transporter) Transport(image *manager.MultiArchImage) (parser []string, err error) {
	t.SourceImage = image.GetFirstImage()
	t.refreshTargetImage()
	if err = t.login(); err != nil {
		err = errors.Wrap(err, "登录仓库失败")
		return
	}
	if image.IsBundle() {
		parser, err = t.tp(true)
		err = errors.Wrap(err, "解析bundle镜像并传输镜像失败")
	} else {
		_, err = t.tp(false)
		err = errors.Wrap(err, "传输镜像失败")
	}
	if err != nil {
		return
	}
	if image.IsSingleArch() {
		return
	}
	// 多架构镜像处理
	reference := make([]string, 0, len(image.Architectures))
	reference = append(reference, t.TargetImage.ReferenceAutoWithArch())
	for _, arch := range image.Architectures {
		if arch == t.SourceImage.Arch {
			continue
		}
		t.SourceImage.Arch = arch
		t.refreshTargetImage()
		reference = append(reference, t.TargetImage.ReferenceAutoWithArch())
		if _, err = t.tp(false); err != nil {
			err = errors.Wrap(err, "传输镜像失败")
			return
		}
	}
	if err = t.pushManifest(reference); err != nil {
		err = errors.Wrap(err, "推送manifest失败")
		return
	}
	return
}

func (t *Transporter) tp(parser bool) (relativeImageNames []string, err error) {
	defer func() {
		if err = t.clean(); err != nil {
			log.Warn().Err(err).Msg("未完全清理镜像")
			return
		}
	}()
	if err = t.pull(); err != nil {
		err = errors.WithMessagef(err, "拉取镜像失败%s", t.SourceImage.ReferenceClean())
		return
	}
	if err = t.tag(); err != nil {
		err = errors.WithMessagef(err, "构建标签失败%s", t.TargetImage.ReferenceAutoWithArch())
		return
	}
	if err = t.push(); err != nil {
		err = errors.WithMessagef(err, "推送镜像失败%s", t.TargetImage.ReferenceAutoWithArch())
		return
	}
	if parser {
		relativeImageNames, err = t.parse()
	}
	return
}

func (t *Transporter) runCmd(cmdStmt []string) error {
	return t.Exec(cmdStmt, t.Stdout, t.Stderr)
}

func (t *Transporter) login() error {
	cmdStmt := []string{"docker", "login", "-u", t.Registry.Username, "-p", t.Registry.Password.Raw, t.Registry.Address}
	if t.Quiet {
		cmdStmt = append(cmdStmt, "2>/dev/null")
	}
	err := t.runCmd(cmdStmt)
	if err != nil {
		return err
	}
	t.step = StepLogin
	return nil
}

func (t *Transporter) pull() error {
	cmdStmt := []string{"docker", "pull", t.SourceImage.ReferenceClean(), "--platform", t.SourceImage.Arch}
	if t.Quiet {
		cmdStmt = append(cmdStmt, "--quiet")
	}
	log.Debug().Str("cmd", strings.Join(cmdStmt, " ")).Msg("拉取镜像")
	err := t.runCmd(cmdStmt)
	if err != nil {
		return err
	}
	log.Info().Str("arch", t.SourceImage.Arch).Str("image", t.SourceImage.ReferenceClean()).Msg("拉取镜像成功")
	t.step = StepPull
	return nil
}

func (t *Transporter) tag() error {
	cmdStmt := []string{"docker", "tag", t.SourceImage.ReferenceClean(), t.TargetImage.ReferenceAutoWithArch()}
	log.Debug().Str("cmd", strings.Join(cmdStmt, " ")).Msg("构建标签")
	err := t.runCmd(cmdStmt)
	if err != nil {
		return err
	}
	log.Info().Str("image", t.TargetImage.ReferenceAutoWithArch()).Msg("构建标签成功")
	t.step = StepTag
	return nil
}

func (t *Transporter) push() error {
	cmdStmt := []string{"docker", "push", t.TargetImage.ReferenceAutoWithArch()}
	if t.Quiet {
		cmdStmt = append(cmdStmt, "--quiet")
	}
	log.Debug().Str("cmd", strings.Join(cmdStmt, " ")).Msg("推送镜像")
	err := t.runCmd(cmdStmt)
	if err != nil {
		return err
	}
	log.Info().Str("image", t.TargetImage.ReferenceAutoWithArch()).Msg("推送镜像成功")
	t.step = StepPush
	return nil
}

func (t *Transporter) clean() error {
	if t.step < StepPull {
		return nil
	}
	cmdStmt := []string{"docker", "rmi", t.SourceImage.ReferenceClean()}
	if t.step >= StepTag {
		cmdStmt = append(cmdStmt, t.TargetImage.ReferenceAutoWithArch())
	}
	log.Debug().Str("cmd", strings.Join(cmdStmt, " ")).Msg("清理镜像")
	err := t.runCmd(cmdStmt)
	if err != nil {
		return err
	}
	log.Info().Str("image", t.SourceImage.Reference.Path()).Msg("清理镜像成功")
	return nil
}

func (t *Transporter) pushManifest(imageReference []string) (err error) {
	cmdStmt := []string{"docker", "manifest", "create", "--insecure", t.TargetImage.ReferenceClean()}
	cmdStmt = append(cmdStmt, imageReference...)
	log.Debug().Str("cmd", strings.Join(cmdStmt, " ")).Msg("构建manifest")
	err = t.runCmd(cmdStmt)
	if err != nil {
		return
	}
	log.Info().Str("image", t.TargetImage.ReferenceClean()).Msg("创建manifest成功")
	if !t.Quiet {
		fmt.Println("########## manifest inspect ##########")
		cmdStmt = []string{"docker", "manifest", "inspect", t.TargetImage.ReferenceClean()}
		err = t.runCmd(cmdStmt)
		if err != nil {
			return
		}
		fmt.Println("########## manifest inspect ##########")
	}
	defer func() {
		log.Debug().Str("cmd", strings.Join(cmdStmt, " ")).Msg("删除manifest")
		err = t.runCmd(cmdStmt)
		if err != nil {
			return
		}
		log.Info().Str("image", t.TargetImage.ReferenceClean()).Msg("删除manifest成功")
	}()
	cmdStmt = []string{"docker", "manifest", "push", "--insecure", t.TargetImage.ReferenceClean()}
	log.Debug().Str("cmd", strings.Join(cmdStmt, " ")).Msg("推送manifest")
	err = t.runCmd(cmdStmt)
	if err != nil {
		return
	}
	log.Info().Str("image", t.TargetImage.ReferenceClean()).Msg("推送manifest成功")
	cmdStmt = []string{"docker", "manifest", "rm", t.TargetImage.ReferenceClean()}
	return
}

func (t *Transporter) parse() ([]string, error) {
	// 使用docker save 命令把镜像转换成tar包
	err := t.save()
	defer os.RemoveAll(t.savePath)
	if err != nil {
		return nil, err
	}
	err = t.decompression(t.savePath)
	defer os.RemoveAll(strings.Split(t.savePath, ".")[0])
	if err != nil {
		return nil, err
	}
	images, err := t.getRelativeImages()
	if err != nil {
		return nil, err
	}
	return images, nil
}

func (t *Transporter) save() error {
	cmdStmt := []string{"mkdir", "-p", t.workspace}
	err := t.runCmd(cmdStmt)
	if err != nil {
		return err
	}
	t.savePath = path.Join(t.workspace, t.SourceImage.Reference.Application()+".tar")
	cmdStmt = []string{"docker", "save", t.SourceImage.ReferenceClean(), "-o", t.savePath}
	log.Debug().Str("cmd", strings.Join(cmdStmt, " ")).Msg("保存镜像")
	err = t.runCmd(cmdStmt)
	if err != nil {
		return err
	}
	log.Info().Str("image", t.SourceImage.ReferenceClean()).Msg("保存镜像成功")
	return nil
}

func (t *Transporter) decompression(filePath string) error {
	destDir := strings.Split(filePath, ".")[0]
	cmdStmt := []string{"mkdir", "-p", destDir}
	err := t.runCmd(cmdStmt)
	if err != nil {
		return err
	}
	cmdStmt = []string{"tar", "-xvf", filePath, "-C", destDir}
	log.Debug().Str("cmd", strings.Join(cmdStmt, " ")).Str("path", filePath).Msg("解压文件")
	err = t.runCmd(cmdStmt)
	if err != nil {
		return err
	}
	log.Info().Str("image", t.SourceImage.ReferenceClean()).Str("path", filePath).Msg("解压文件成功")
	return nil
}

func (t *Transporter) getRelativeImages() ([]string, error) {
	destDir := strings.Split(t.savePath, ".")[0]
	manifestPath := path.Join(destDir, "manifest.json")
	data, err := t.ReadFile(manifestPath)
	if err != nil {
		err = errors.Wrap(err, "读取manifest文件失败")
		return nil, err
	}
	manifests := make([]*docker.Manifest, 0)
	err = json.Unmarshal(data, &manifests)
	if err != nil {
		return nil, err
	}
	firstLayoutPath := path.Join(destDir, manifests[0].Layers[0])
	err = t.decompression(firstLayoutPath)
	if err != nil {
		err = errors.Wrap(err, "解压文件失败")
		return nil, err
	}
	deDir := strings.Split(firstLayoutPath, ".")[0]
	csvName := strings.TrimSuffix(t.SourceImage.Reference.Application(), "-bundle") + ".clusterserviceversion.yaml"
	csvPath := path.Join(deDir, "manifests", csvName)
	data, err = t.ReadFile(csvPath)
	if err != nil {
		err = errors.Wrap(err, "读取csv文件失败")
		return nil, err
	}
	csv := new(docker.ClusterServiceVersion)
	err = yaml.Unmarshal(data, csv)
	if err != nil {
		return nil, err
	}
	images := make([]string, 0, len(csv.Spec.RelatedImages))
	for _, image := range csv.Spec.RelatedImages {
		images = append(images, image.Image)
	}
	return images, nil
}
